//-----------------------------------------------------------------------
// <copyright file="ZipManager.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation.  All rights reserved.
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY 
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
// </copyright>
// <summary>Implementation of zip file utilities.</summary>
//-----------------------------------------------------------------------
namespace Microsoft.Sdc.Tasks.Zip
{
    using System;
    using System.Collections;
    using System.Globalization;
    using System.IO;
    using java.io;
    using java.util.zip;

    /// <summary>
    /// Provides methods for manipulating Zip files
    /// </summary>
    internal static class ZipManager
    {
        /// <summary>
        /// Create the Zip file
        /// </summary>
        /// <param name="pathToZipFile">File path for Zip file.</param>
        public static void Create(string pathToZipFile)
        {
            new ZipOutputStream(new FileOutputStream(pathToZipFile)).close();
        }

        /// <summary>
        /// Extract contents
        /// </summary>
        /// <param name="pathToZipFile">File path for Zip file.</param>
        /// <param name="path">Extract path.</param>
        /// <param name="fileToExtract">File to extract.</param>
        /// <param name="skipExisting">Skip existing file.</param>
        public static void Extract(string pathToZipFile, string path, string fileToExtract, bool skipExisting)
        {
            ExtractInternal(pathToZipFile, path, new string[] { fileToExtract }, false, skipExisting);
        }

        /// <summary>
        /// Extract contents
        /// </summary>
        /// <param name="pathToZipFile">File path for Zip file.</param>
        /// <param name="pathToFile">Extract path.</param>
        /// <param name="skipExisting">Skip existing file.</param>
        public static void Extract(string pathToZipFile, string pathToFile, bool skipExisting)
        {
            ExtractInternal(pathToZipFile, pathToFile, null, true, skipExisting);
        }

        /// <summary>
        /// Extract contents
        /// </summary>
        /// <param name="pathToZipFile">File path for Zip file.</param>
        /// <param name="path">Extract path.</param>
        /// <param name="filesToExtract">Files to extract.</param>
        /// <param name="skipExisting">Skip existing file.</param>
        public static void Extract(string pathToZipFile, string path, string[] filesToExtract, bool skipExisting)
        {
            ExtractInternal(pathToZipFile, path, filesToExtract, false, skipExisting);
        }

        /// <summary>
        /// Add file to the Zip
        /// </summary>
        /// <param name="pathToZipFile">File path for Zip file.</param>
        /// <param name="pathToFile">Path to file to add.</param>
        /// <param name="includeFolderPath">Include the file's path in the archive.</param>
        /// <param name="rootFolder">Folder to use as root for archive.</param>
        public static void Add(string pathToZipFile, string pathToFile, bool includeFolderPath, string rootFolder)
        {
            Add(pathToZipFile, new string[] { pathToFile }, includeFolderPath, rootFolder);
        }

        /// <summary>
        /// Add file to the Zip
        /// </summary>
        /// <param name="pathToZipFile">File path for Zip file.</param>
        /// <param name="pathsToFiles">Paths to files to add.</param>
        /// <param name="includeFolderPath">Include the file's path in the archive.</param>
        /// <param name="rootFolder">Folder to use as root for archive.</param>
        public static void Add(string pathToZipFile, string[] pathsToFiles, bool includeFolderPath, string rootFolder)
        {
            if (!System.IO.File.Exists(pathToZipFile))
            {
                Create(pathToZipFile);
            }

            ZipFile file = OpenFile(pathToZipFile);

            string existingZipFileName = file.getName();
            string tempZipFileName = Path.GetTempFileName();
            string currentFilename = null;
            ZipOutputStream destination = new ZipOutputStream(new FileOutputStream(tempZipFileName));
            try
            {
                CopyZipEntries(file, destination);
                if (pathsToFiles != null)
                {
                    foreach (string filename in pathsToFiles)
                    {
                        
                        currentFilename = filename;

                        // If it's a folder path then loop through it adding all the files
                        if (Directory.Exists(filename))
                        {
                            AddDirectory(filename, destination, includeFolderPath, rootFolder);
                        }
                        else if (System.IO.File.Exists(filename))
                        {
                            AddFile(filename, destination, includeFolderPath, rootFolder);
                        }
                    }
                }
            }
            catch (ZipException e)
            {
                throw new ApplicationException(String.Format(CultureInfo.InvariantCulture, "Unable to add file '{0}' to zip file '{1}'. It may already exist.", currentFilename, pathToZipFile), e);
            }
            finally
            {
                destination.close();
            }

            file.close();
            System.IO.File.Copy(tempZipFileName, existingZipFileName, true);
            System.IO.File.Delete(tempZipFileName);
        }

        /// <summary>
        /// Add directory to the Zip
        /// </summary>
        /// <param name="directory">Directory to add.</param>
        /// <param name="destination">The output destination.</param>
        /// <param name="includeFolderPath">Include the file's path in the archive.</param>
        /// <param name="rootFolder">Folder to use as root for archive.</param>
        private static void AddDirectory(string directory, ZipOutputStream destination, bool includeFolderPath, string rootFolder)
        {
            DirectoryInfo di = new DirectoryInfo(directory);
            FileInfo[] fileInfos = di.GetFiles();
            foreach (FileInfo fi in fileInfos)
            {
                AddFile(fi.FullName, destination, includeFolderPath, rootFolder);
            }

            DirectoryInfo[] directoryInfos = di.GetDirectories();
            foreach (DirectoryInfo di2 in directoryInfos)
            {
                AddDirectory(di2.FullName, destination, includeFolderPath, rootFolder);
            }
        }

        /// <summary>
        /// Add file to the Zip
        /// </summary>
        /// <param name="filename">Name of file to add.</param>
        /// <param name="destination">The output destination.</param>
        /// <param name="includeFolderPath">Include the file's path in the archive.</param>
        /// <param name="rootFolder">Folder to use as root for archive.</param>
        private static void AddFile(string filename, ZipOutputStream destination, bool includeFolderPath, string rootFolder)
        {
            string pathRoot = Path.GetPathRoot(filename);
            string zipEntryName;

            if (!includeFolderPath)
            {
                // do not include any path information in the Archive
                zipEntryName = Path.GetFileName(filename);
            }
            else if (String.Empty != rootFolder)
            {
                // rooted archive. Only include folder names in the archive below the specified root
                if (rootFolder[rootFolder.Length - 1] == Path.DirectorySeparatorChar)
                {
                    rootFolder = rootFolder.Substring(0, rootFolder.Length - 1);
                }

                // Check that the specified root is correct
                if (Path.GetDirectoryName(filename).Substring(0, Path.GetFullPath(rootFolder).Length) != Path.GetFullPath(rootFolder))
                {
                    // Specified root does not exists!
                    throw new TaskException(string.Format("{0} not found.", rootFolder));
                }

                zipEntryName = Path.Combine(
                    Path.GetDirectoryName(filename).Substring(Path.GetFullPath(rootFolder).Length),
                    Path.GetFileName(filename));

                if (zipEntryName[0] == Path.DirectorySeparatorChar)
                {
                    zipEntryName = zipEntryName.Substring(1);
                }
            }
            else
            {
                // build archive "as-is"
                zipEntryName = pathRoot.StartsWith(@"\\") ? filename.Remove(0, Path.GetPathRoot(filename).Length + 1) : filename.Remove(0, Path.GetPathRoot(filename).Length);
            }

            ZipEntry zipEntry = new ZipEntry(zipEntryName);
            zipEntry.setMethod(ZipEntry.DEFLATED);
            destination.putNextEntry(zipEntry);
            try
            {
                FileInputStream stream = new FileInputStream(filename);
                try
                {
                    CopyStream(stream, destination);
                }
                finally
                {
                    stream.close();
                }
            }
            finally
            {
                destination.closeEntry();
            }
        }

        /// <summary>
        /// Extract contents of Zip.
        /// </summary>
        /// <param name="pathToZipFile">Pathto the Zip file.</param>
        /// <param name="pathToDestinationFolder">Destination to extract to.</param>
        /// <param name="filesToExtract">List of files to extract.</param>
        /// <param name="extractAll">Extract all files.</param>
        /// <param name="skipExisting">Skip exisiting files.</param>
        private static void ExtractInternal(string pathToZipFile, string pathToDestinationFolder, string[] filesToExtract, bool extractAll, bool skipExisting)
        {
            ZipFile file = OpenFile(pathToZipFile);
            foreach (ZipEntry zipEntry in new JSEnumerationWrapper(file.entries()))
            {
                string zipEntryName = zipEntry.getName();
                if (!zipEntry.isDirectory())
                {
                    if (filesToExtract == null ||
                        extractAll ||
                        Array.BinarySearch(filesToExtract, zipEntryName, CaseInsensitiveComparer.Default) >= 0)
                    {
                        string fileName = Path.GetFileName(zipEntryName);
                        string newPath = Path.Combine(pathToDestinationFolder, Path.GetDirectoryName(zipEntryName));
                        string fullPath = Path.Combine(newPath, fileName);

                        DateTime zipFileDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0);
                        double zipFileUnixTime = zipEntry.getTime();
                        zipFileDateTime = zipFileDateTime.AddMilliseconds(zipFileUnixTime);

                        // Don't bother extracting file if its already there with same or newer date
                        if (System.IO.File.Exists(fullPath) && skipExisting)
                        {
                            if (zipFileDateTime <= System.IO.File.GetCreationTime(fullPath))
                            {
                                continue;
                            }
                        }

                        InputStream stream = file.getInputStream(zipEntry);
                        try
                        {
                            Directory.CreateDirectory(newPath);
                            FileOutputStream dest = new FileOutputStream(fullPath);
                            try
                            {
                                CopyStream(stream, dest);
                            }
                            finally
                            {
                                dest.close();
                            }

                            System.IO.File.SetCreationTime(fullPath, zipFileDateTime);
                        }
                        finally
                        {
                            stream.close();
                        }
                    }
                }
            }

            file.close();
        }

        /// <summary>
        /// Open a Zip file.
        /// </summary>
        /// <param name="pathToZipFile">Path to the Zip file.</param>
        /// <returns>The opened Zip file.</returns>
        private static ZipFile OpenFile(string pathToZipFile)
        {
            return new ZipFile(pathToZipFile);
        }

        /// <summary>
        /// Copy contents of Zip.
        /// </summary>
        /// <param name="source">Source.</param>
        /// <param name="destination">Destination.</param>
        private static void CopyZipEntries(ZipFile source, ZipOutputStream destination)
        {
            foreach (ZipEntry zipEntry in new JSEnumerationWrapper(source.entries()))
            {
                InputStream stream = source.getInputStream(zipEntry);
                try
                {
                    destination.putNextEntry(zipEntry);
                    try
                    {
                        CopyStream(stream, destination);
                    }
                    finally
                    {
                        destination.closeEntry();
                    }
                }
                finally
                {
                    stream.close();
                }
            }
        }

        /// <summary>
        /// Copy stream.
        /// </summary>
        /// <param name="source">Source.</param>
        /// <param name="destination">Destination.</param>
        private static void CopyStream(InputStream source, OutputStream destination)
        {
            sbyte[] buffer = new sbyte[8192];
            int bytesRead;
            while ((bytesRead = source.read(buffer, 0, buffer.Length)) > 0)
            {
                destination.write(buffer, 0, bytesRead);
            }
        }
    }
}